{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# softmax回归的简洁实现\n",
    ":label:`sec_softmax_concise`\n",
    "\n",
    "在 :numref:`sec_linear_concise` 中，我们可以发现使用 DJL 能够使跟简洁实现线性回归\n",
    "。同样地，使用 DJL 也能更方便地实现分类模型。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "origin_pos": 1,
    "tab": [
     "mxnet"
    ]
   },
   "outputs": [],
   "source": [
    "%load ../utils/djl-imports\n",
    "\n",
    "import ai.djl.basicdataset.cv.classification.*;\n",
    "import ai.djl.metric.*;"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "让我们继续使用Fashion-MNIST数据集，并保持批量大小为256，就像在 :numref:`sec_softmax_scratch` 中一样。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "origin_pos": 4,
    "tab": [
     "mxnet"
    ]
   },
   "outputs": [],
   "source": [
    "int batchSize = 256;\n",
    "boolean randomShuffle = true;\n",
    "\n",
    "// Get Training and Validation Datasets\n",
    "FashionMnist trainingSet = FashionMnist.builder()\n",
    "        .optUsage(Dataset.Usage.TRAIN)\n",
    "        .setSampling(batchSize, randomShuffle)\n",
    "        .optLimit(Long.getLong(\"DATASET_LIMIT\", Long.MAX_VALUE))\n",
    "        .build();\n",
    "\n",
    "\n",
    "FashionMnist validationSet = FashionMnist.builder()\n",
    "        .optUsage(Dataset.Usage.TEST)\n",
    "        .setSampling(batchSize, false)\n",
    "        .optLimit(Long.getLong(\"DATASET_LIMIT\", Long.MAX_VALUE))\n",
    "        .build();"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 初始化模型参数\n",
    "\n",
    "如我们在 :numref:`sec_softmax` 所述，softmax 回归的输出层是一个全连接层。因此，为了实现我们的模型，我们只需在 `SequentialBlock` 中添加一个带有10个输出的全连接层。同样，在这里，`SequentialBlock` 并不是必要的，但我们可能会形成这种习惯。因为在实现深度模型时，`SequentialBlock`将无处不在。我们仍然以均值0和标准差0.01随机初始化权重。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "origin_pos": 6,
    "tab": [
     "mxnet"
    ]
   },
   "outputs": [],
   "source": [
    "public class ActivationFunction {\n",
    "    public static NDList softmax(NDList arrays) {\n",
    "        return new NDList(arrays.singletonOrThrow().logSoftmax(1));\n",
    "    }\n",
    "}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "NDManager manager = NDManager.newBaseManager();\n",
    "\n",
    "Model model = Model.newInstance(\"softmax-regression\");\n",
    "\n",
    "SequentialBlock net = new SequentialBlock();\n",
    "net.add(Blocks.batchFlattenBlock(28 * 28)); // flatten input\n",
    "net.add(Linear.builder().setUnits(10).build()); // set 10 output channels\n",
    "\n",
    "model.setBlock(net);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 重新审视Softmax的实现\n",
    ":label:`subsec_softmax-implementation-revisited`\n",
    "\n",
    "在前面 :numref:`sec_softmax_scratch` 的例子中，我们计算了模型的输出，然后将此输出送入交叉熵损失。从数学上讲，这是一件完全合理的事情。然而，从计算角度来看，指数可能会造成数值稳定性问题。\n",
    "\n",
    "回想一下，softmax函数 $\\hat y_j = \\frac{\\exp(o_j)}{\\sum_k \\exp(o_k)}$，其中$\\hat y_j$是预测的概率分布。$o_j$是未归一化的预测$\\mathbf{o}$的第$j$个元素。如果$o_k$中的一些数值非常大，那么 $\\exp(o_k)$ 可能大于数据类型容许的最大数字（即 *上溢*（overflow））。这将使分母或分子变为`inf`（无穷大），我们最后遇到的是0、`inf` 或 `nan`（不是数字）的 $\\hat y_j$。在这些情况下，我们不能得到一个明确定义的交叉熵的返回值。\n",
    "\n",
    "解决这个问题的一个技巧是，在继续softmax计算之前，先从所有$o_k$中减去$\\max(o_k)$。你可以证明每个 $o_k$ 按常数进行的移动不会改变softmax的返回值。在减法和归一化步骤之后，可能有些 $o_j$ 具有较大的负值。由于精度受限， $\\exp(o_j)$ 将有接近零的值，即 *下溢*（underflow）。这些值可能会四舍五入为零，使 $\\hat y_j$ 为零，并且使得 $\\log(\\hat y_j)$ 的值为 `-inf`。反向传播几步后，我们可能会发现自己面对一屏幕可怕的`nan`结果。\n",
    "\n",
    "尽管我们要计算指数函数，但我们最终在计算交叉熵损失时会取它们的对数。\n",
    "通过将softmax和交叉熵结合在一起，可以避免反向传播过程中可能会困扰我们的数值稳定性问题。如下面的等式所示，我们避免计算$\\exp(o_j)$，而可以直接使用$o_j$。因为$\\log(\\exp(\\cdot))$被抵消了。\n",
    "\n",
    "$$\n",
    "\\begin{aligned}\n",
    "\\log{(\\hat y_j)} & = \\log\\left( \\frac{\\exp(o_j)}{\\sum_k \\exp(o_k)}\\right) \\\\\n",
    "& = \\log{(\\exp(o_j))}-\\log{\\left( \\sum_k \\exp(o_k) \\right)} \\\\\n",
    "& = o_j -\\log{\\left( \\sum_k \\exp(o_k) \\right)}.\n",
    "\\end{aligned}\n",
    "$$\n",
    "\n",
    "我们也希望保留传统的softmax函数，以备我们需要评估通过模型输出的概率。\n",
    "但是，我们没有将softmax概率传递到损失函数中，而是[**在交叉熵损失函数中传递未归一化的预测，并同时计算softmax及其对数**]，这是一件聪明的事情 [\"LogSumExp技巧\"](https://en.wikipedia.org/wiki/LogSumExp)。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "origin_pos": 10,
    "tab": [
     "mxnet"
    ]
   },
   "outputs": [],
   "source": [
    "Loss loss = Loss.softmaxCrossEntropyLoss();"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 优化算法\n",
    "\n",
    "在这里，我们使用学习率为0.1的小批量随机梯度下降作为优化算法。这与我们在线性回归例子中的相同，这说明了优化器的普适性。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "origin_pos": 14,
    "tab": [
     "mxnet"
    ]
   },
   "outputs": [],
   "source": [
    "Tracker lrt = Tracker.fixed(0.1f);\n",
    "Optimizer sgd = Optimizer.sgd().setLearningRateTracker(lrt).build();"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## `Trainer`的初始化配置\n",
    "\n",
    "下面这段程序，展示了我们如何初始化及配置`trainer`，并用这个`trainer`对人工智能模型进行训练。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "origin_pos": 18,
    "tab": [
     "mxnet"
    ]
   },
   "outputs": [],
   "source": [
    "DefaultTrainingConfig config = new DefaultTrainingConfig(loss)\n",
    "    .optOptimizer(sgd) // Optimizer\n",
    "    .optDevices(manager.getEngine().getDevices(1)) // single GPU\n",
    "    .addEvaluator(new Accuracy()) // Model Accuracy\n",
    "    .addTrainingListeners(TrainingListener.Defaults.logging()); // Logging\n",
    "\n",
    "Trainer trainer = model.newTrainer(config);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 初始化模型参数\n",
    "\n",
    "我们调用 `initialize` 函数，对模型及模型参数进行初始化。 shape ($1$, $748$)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "trainer.initialize(new Shape(1, 28 * 28)); // Input Images are 28 x 28"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 运行性能指标\n",
    "\n",
    "一般情况下，DJL 不会自动记录运行性能指标，因为记录运行性能指标本身会提高运行成本，降低运行性能。如果出于特殊理由，你需要对某些运行指标进行记录，你可以生成一个 `metrics` 并将这个新生成的 `metrics` 设置成 `trainer` 的 `metrics` 即可，具体程序如下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "Metrics metrics = new Metrics();\n",
    "trainer.setMetrics(metrics);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 训练\n",
    "\n",
    "在 :numref:`sec_linear_concise` 中，我们使用 `EasyTrain` 类来简化训练的代码，其实我们可以使用 `EasyTrain.fit()` 函数进一步简化代码："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "int numEpochs = 3;\n",
    "\n",
    "EasyTrain.fit(trainer, numEpochs, trainingSet, validationSet);\n",
    "var result = trainer.getTrainingResult();"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "和以前一样，这个算法收敛到一个相当高的精度，而且这次的代码行比以前少了。\n",
    "\n",
    "## 小结\n",
    "\n",
    "* 我们可以使用 DJL `EasyTrain.fit()` 函数更简洁地实现 softmax 回归。\n",
    "* 从计算的角度来看，实现softmax回归比较复杂。在许多情况下，深度学习框架在这些著名的技巧之外采取了额外的预防措施，来确保数值的稳定性。这使我们避免了在实践中从零开始编写模型时可能遇到的陷阱。\n",
    "\n",
    "## 练习\n",
    "\n",
    "1. 尝试调整超参数，例如批量大小、迭代周期数和学习率，并查看结果。\n",
    "1. 增加迭代周期的数量。为什么测试准确率会在一段时间后降低？我们怎么解决这个问题？\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Java",
   "language": "java",
   "name": "java"
  },
  "language_info": {
   "codemirror_mode": "java",
   "file_extension": ".jshell",
   "mimetype": "text/x-java-source",
   "name": "Java",
   "pygments_lexer": "java",
   "version": "14.0.2+12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
